package alibaba.server;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FilenameUtils;


import alibaba.shared.Base64;
import alibaba.shared.Utils;
import static alibaba.shared.Utils.*;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

@SuppressWarnings("restriction")
public class AlibabaServer implements HttpHandler {

	private static final String LISTING_DATA_FILE_NAME = "listing.dat";

	private static final String FILE_NAME_FORMAT = "file_%d.bak";
	private int nextFileNumber = 0;

	private static final String CHECK_OUTPUT_FORMAT = "%s , %s\n";

	private static final byte[] ZERO_BYTE = {0};

	private Listing listing;
	private File backupDir;

	public static void main(String[] args) throws IOException {
		if (args.length != 1) {
			System.err.println("Usage: java -jar AlibabaServer.jar <backup directory name>");
			return;
		}
		new AlibabaServer(args[0]).start();
	}

	private AlibabaServer(String backupDirPath) throws IOException {
		this.backupDir = new File(backupDirPath);
		if (this.backupDir.exists() && !this.backupDir.isDirectory()) {
			throw new IOException("The path: " + backupDirPath + " Exists, and is not a directory.");
		} else if (!this.backupDir.exists()) {
			System.out.println(SERVER_PRINTING_PREFIX + "Backup directory doesn't exist, creating one...");
			backupDir.mkdirs();
		}
		File listingFile = new File(FilenameUtils.concat(backupDir.getAbsolutePath(), LISTING_DATA_FILE_NAME));
		if (!listingFile.exists()) {
			System.out.println(SERVER_PRINTING_PREFIX + "Listing file is not present. Creating a new one");
			this.listing = new Listing();
			Listing.writeListing(listingFile.getAbsolutePath(), this.listing);
		} else {
			this.listing = Listing.readListing(listingFile.getAbsolutePath());
		}
	}

	private void start() throws IOException {
		if (!doConsistancyCheck()) {
			System.err.println(SERVER_PRINTING_PREFIX + "Consistancy check failed... Exiting.");
			return;
		}

		InetSocketAddress addr = new InetSocketAddress(8080);
		HttpServer server = HttpServer.create(addr, 0);
		server.createContext("/", this);
		server.start();
		System.out.println(SERVER_PRINTING_PREFIX + "Server is listening...");
	}

	private static int getFileNumber(String filename) {
		Pattern p = Pattern.compile("^" + FILE_NAME_FORMAT.replace("%d", "(\\d+)") + "$");
		Matcher m = p.matcher(filename);
		if (m.find()) {
			return Integer.parseInt(m.group(1));
		}
		return -1;
	}

	private boolean doConsistancyCheck() {
		for (Map.Entry<Listing.FileNamePair, ListingEntry> entry : listing.getListOfFiles().entrySet()) {
			int fileNumber = getFileNumber(entry.getValue().getLocalName());
			if (fileNumber >= nextFileNumber) {
				nextFileNumber = fileNumber + 1;
			}
			if (!entry.getKey().getFilename().equals(entry.getValue().getFilename()) ||
					!entry.getKey().getOwner().equals(entry.getValue().getOwner())) {
				System.err.println("File name/owner in registry doesn't match.");
				return false;
			}
			File file = new File(entry.getValue().getLocalName());
			if (!file.isFile()) {
				System.err.println(("File " + file.getAbsolutePath() + " was not found"));
				return false;
			}
			try {
				if (!Arrays.equals(Utils.getFileHash(file), entry.getValue().getFileHash())) {
					System.out.println(SERVER_PRINTING_PREFIX + "File signature doesn't match for: " + entry.getKey());
					entry.getValue().setFileHash(ZERO_BYTE);
					saveListing();
					continue;
				}
			} catch (IOException e) {
				e.printStackTrace();
				return false;
			}
		}
		return true;
	}

	private Map<String, byte[]> doCheck(String dir, String owner) throws IOException {
		Map<String, byte[]> pairs = new HashMap<String, byte[]>();
		for (ListingEntry entry : listing.getListOfFiles().values()) {
			if (!entry.getOwner().equals(owner) || !isEntryInDir(entry, dir)) {
				continue;
			}
			if (!Arrays.equals(Utils.getFileHash(new File(entry.getLocalName())), entry.getFileHash())) {
				entry.setFileHash(ZERO_BYTE);
				saveListing();
			}
			pairs.put(entry.getFilename(), entry.getFileHash());
		}
		return pairs;
	}

	private void saveListing() throws FileNotFoundException {
		Listing.writeListing(FilenameUtils.concat(backupDir.getAbsolutePath(), LISTING_DATA_FILE_NAME), this.listing);
	}

	private void backupFile(String filename, byte[] data, String owner) throws IOException {
		File file = new File(FilenameUtils.concat(backupDir.getAbsolutePath(), String.format(FILE_NAME_FORMAT, nextFileNumber)));
		nextFileNumber += 1;
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
		bos.write(data);
		bos.close();
		ListingEntry listingEntry = new ListingEntry(filename, file.getAbsolutePath(), Utils.getDataHash(data), owner);
		this.listing.addFileEntry(listingEntry);
		saveListing();
	}

	private void backupDedupFile(String filename, String owner, Listing.FileNamePair oldFile) throws FileNotFoundException {
		if (filename.equals(oldFile.getFilename()) && owner.equals(oldFile.getOwner())) {
			return;
		}
		ListingEntry oldEntry = this.listing.getFileEntry(oldFile);
		ListingEntry listingEntry = new ListingEntry(filename, oldEntry.getLocalName(), oldEntry.getFileHash(), owner);
		this.listing.addFileEntry(listingEntry);
		saveListing();
	}

	private String getFileContent(String filename, String owner) throws IOException {
		File file = new File(listing.getFileEntry(filename, owner).getLocalName());
		return new String(Utils.readFileIntoBuf(file));
	}

	private boolean isEntryInDir(ListingEntry entry, String dir) {
		File dirFile = new File(dir);
		File file = new File(entry.getFilename());
		if (file.getParent().equals(dirFile.getPath())) {
			return true;
		}
		return false;
	}

	private List<String> listFilesInDirectory(String dir, String owner) {
		List<String> files = new ArrayList<String>();
		for (ListingEntry entry : listing.getListOfFiles().values()) {
			if (entry.getOwner().equals(owner) && isEntryInDir(entry, dir)) {
				files.add(entry.getFilename());
			}
		}
		return files;
	}

	private byte[] getFileSignature(String filename, String owner) {
		return listing.getFileEntry(filename, owner).getFileHash();
	}

	private boolean isFileInListing(String filename, String owner) {
		return listing.getFileEntry(filename, owner) != null;
	}

	private Listing.FileNamePair getFileForSignature(byte[] signature) {
		for (Map.Entry<Listing.FileNamePair, ListingEntry> entry: this.listing.getListOfFiles().entrySet()) {
			if (Arrays.equals(entry.getValue().getFileHash(), signature)) {
				return entry.getKey();
			}
		}
		return null;
	}

	@Override
	public void handle(HttpExchange exchange) throws IOException {
		String method = exchange.getRequestMethod();
		String path = exchange.getRequestURI().getPath();
		String query = exchange.getRequestURI().getQuery();
		String user = exchange.getRequestHeaders().getFirst(AUTHORIZATION_STR);
		if (user == null) {
			System.out.println("Got request without authorization.");
			sendError(exchange, 400, "Authorization is not specified");
			return;
		}

		try {
			if (method.equalsIgnoreCase("GET")) {
				System.out.print(Utils.SERVER_PRINTING_PREFIX + "Got a GET request for: " + path);
				if (query != null) {
					System.out.println(" to do " + query);
					if (query.equalsIgnoreCase(Utils.SIGNATURE_QUERY_STR)) {
						if (isFileInListing(path, user)) {
							String encodedSignature = Base64.encodeBytes(getFileSignature(path, user), Base64.URL_SAFE);
							sendResponse(exchange, encodedSignature + "\n");
						} else {
							sendError(exchange, 404, path + " was not found.");
							return;
						}
					} else if (query.equalsIgnoreCase(Utils.CHECK_QUERY_STR)) {
						StringBuilder responseBuilder = new StringBuilder();
						for (Map.Entry<String, byte[]> entry : doCheck(path, user).entrySet()) {
							String fileHashBase64 = Base64.encodeBytes(entry.getValue(), Base64.URL_SAFE);
							responseBuilder.append(String.format(CHECK_OUTPUT_FORMAT, entry.getKey(), fileHashBase64));
						}
						sendResponse(exchange, responseBuilder.toString());
					} else {
						String error = "Got strange query: " + query;
						System.err.println(error);
						sendError(exchange, 400, error);
						return;
					}
				} else {
					System.out.println();
					// It's either restoring a file or getting directory listing.
					if (isFileInListing(path, user)) {
						// This is a file, let's return it.
						try {
							sendResponse(exchange, getFileContent(path, user));
						} catch (Exception e) {
							e.printStackTrace();
							sendError(exchange, 500, e.getMessage());
							return;
						}
					} else {
						// This might be a directory, let's try to return it.
						List<String> directoryList = listFilesInDirectory(path, user);
						if (directoryList.isEmpty() && !path.endsWith("/")) {
							sendError(exchange, 404, "File not found");
							return;
						}
						StringBuilder responseBuilder = new StringBuilder();
						for (String dir : directoryList) {
							responseBuilder.append(dir + "\n");
						}
						sendResponse(exchange, responseBuilder.toString());
					}
				}
			} else if (method.equalsIgnoreCase("PUT")) {
				System.out.print(Utils.SERVER_PRINTING_PREFIX + "Got a PUT request for: " + path);
				if (query != null) {
					System.out.println(" to do " + query);
					if (query.toLowerCase().startsWith(SIGNATURE_QUERY_STR + "=")) {
						byte[] signature = Base64.decode(query.substring(SIGNATURE_QUERY_STR.length() + 1), Base64.URL_SAFE);
						Listing.FileNamePair fileNamePair = getFileForSignature(signature);
						if (fileNamePair != null) {
							backupDedupFile(path, user, fileNamePair);
							sendOK(exchange);
							return;
						} else {
							sendError(exchange, 404, "signature was not found.");
							return;
						}
					} else {
						String error = "Got strange query: " + query;
						System.err.println(error);
						sendError(exchange, 400, error);
						return;
					}
				} else {
					System.out.println();
					try {
						backupFile(path, Utils.readInputIntoBuf(exchange.getRequestBody()), user);
					} catch (IOException e) {
						e.printStackTrace();
						sendError(exchange, 500, e.getMessage());
						return;
					}
					sendOK(exchange);
				}
			} else {
				String error = "Got strange request method: " + method;
				System.err.println(error);
				exchange.getResponseHeaders().add("Allow", "GET");
				exchange.getResponseHeaders().add("Allow", "PUT");
				sendError(exchange, 405, error);
				return;
			}
		} catch (IOException e) {
			e.printStackTrace();
			sendError(exchange, 500, "Couldn't process request: " + e.getMessage());
			return;
		}
	}

	private void sendResponse(HttpExchange exchange, String response) throws IOException {
		exchange.getResponseHeaders().set("Content-Type", "text/plain");
		exchange.sendResponseHeaders(200, 0);
		exchange.getResponseBody().write(response.getBytes());
		exchange.close();
	}

	private void sendOK(HttpExchange exchange) throws IOException {
		exchange.sendResponseHeaders(200, 0);
		exchange.close();
	}

	private void sendError(HttpExchange exchange, int code, String error) throws IOException {
		exchange.sendResponseHeaders(code, 0);
		exchange.getResponseBody().write(error.getBytes());
		exchange.close();
	}
}
